/**
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef PANDA_ECMA_PROFILING_H
#define PANDA_ECMA_PROFILING_H

#include "js_tagged_value.h"
#include "runtime/profiling/profiling.h"
#include "plugins/ecmascript/runtime/ecma_call_profiling_table.h"
#include "utils/bit_field.h"

namespace ark::ecmascript {

class ProfilingTypeBits {
public:
    enum Type : uint8_t {
        NONE = 0,
        INTEGER = (1U << 0U),
        DOUBLE = (1U << 1U),
        BOOLEAN = (1U << 2U),
        STRING = (1U << 3U),
        SPECIAL = (1U << 4U),
        OBJECT = (1U << 5U),
        LAST = OBJECT,
        DOUBLE_INTEGER = DOUBLE | INTEGER,
        // NOLINTNEXTLINE(hicpp-signed-bitwise)
        SPECIAL_INT = SPECIAL | BOOLEAN | INTEGER,  // boolean can be cast to integer
        SPECIAL_DOUBLE = SPECIAL | DOUBLE,
        SPECIAL_NUMBER = SPECIAL_INT | DOUBLE,
        HEAP_OBJECT = STRING | OBJECT
    };

    template <typename S>
    static void Dump(S &stream, Type type)
    {
        if (type == NONE) {
            stream << "[None]";
            return;
        }
        stream << '[';
        const char *sep = "";
        if (type & INTEGER) {
            stream << sep << "Integer";
            sep = "|";
        }
        if (type & DOUBLE) {
            stream << sep << "Double";
            sep = "|";
        }
        if (type & BOOLEAN) {
            stream << sep << "Bool";
            sep = "|";
        }
        if (type & STRING) {
            stream << sep << "String";
            sep = "|";
        }
        if (type & OBJECT) {
            stream << sep << "Object";
            sep = "|";
        }
        stream << ']';
    }
};

class ProfilingTypeOfBits {
public:
    enum Type : uint8_t {
        NONE = 0,
        NUMBER = (1U << 0U),
        SYMBOL = (1U << 1U),
        BOOLEAN = (1U << 2U),
        STRING = (1U << 3U),
        FUNCTION = (1U << 4U),
        UNDEFINED = (1U << 5U),
        BIGINT = (1U << 6U),
        OBJECT = (1U << 7U),
        LAST = OBJECT,
    };
    template <typename S>
    static void Dump(S &stream, Type type)
    {
        if (type == NONE) {
            stream << "[None]";
            return;
        }
        stream << '[';
        const char *sep = "";
        if (type & NUMBER) {
            stream << sep << "Number";
            sep = "|";
        }
        if (type & SYMBOL) {
            stream << sep << "Symbol";
            sep = "|";
        }
        if (type & BOOLEAN) {
            stream << sep << "Bool";
            sep = "|";
        }
        if (type & STRING) {
            stream << sep << "String";
            sep = "|";
        }
        if (type & OBJECT) {
            stream << sep << "Object";
            sep = "|";
        }
        if (type & FUNCTION) {
            stream << sep << "Function";
            sep = "|";
        }
        if (type & UNDEFINED) {
            stream << sep << "Undefined";
            sep = "|";
        }
        if (type & BIGINT) {
            stream << sep << "BigInt";
            sep = "|";
        }
        stream << ']';
    }
};

class ProfilingIndexedAccessBits {
public:
    enum Type : uint8_t {
        NONE = 0,
        OBJECT_ARRAY_ACCESS = (1U << 0U),
        NOT_HEAP_OBJECT_ACCESS = (1U << 1U),
        OBJECT_ACCESS = (1U << 2U),
        LAST = OBJECT_ACCESS
    };

    template <typename S>
    static void Dump(S &stream, Type type)
    {
        if (type == NONE) {
            stream << "[None]";
            return;
        }
        stream << '[';
        const char *sep = "";
        if (type & OBJECT_ARRAY_ACCESS) {
            stream << sep << "objct array access";
            sep = "|";
        }
        if (type & NOT_HEAP_OBJECT_ACCESS) {
            stream << sep << "not object";
            sep = "|";
        }
        if (type & OBJECT_ACCESS) {
            stream << sep << "Object";
            sep = "|";
        }
        stream << ']';
    }
};

template <typename T>
class ProfileInfo {
public:
    using Type = typename T::Type;
    ProfileInfo() = default;
    // NOLINTNEXTLINE(google-explicit-constructor)
    ProfileInfo(Type type) : type_(type) {}
    // NOLINTNEXTLINE(google-explicit-constructor)
    ProfileInfo(std::underlying_type_t<Type> type) : type_(static_cast<Type>(type)) {}

    template <typename S>
    void Dump(S &stream)
    {
        T::Dump(stream, type_);
    }

    Type GetType() const
    {
        return type_;
    }

    bool Contains(Type other) const
    {
        return (type_ & other.type_) != 0;
    }

    bool operator==(Type other)
    {
        return type_ == other.type_;
    }

    // NOLINTNEXTLINE(google-explicit-constructor)
    operator T() const
    {
        return type_;
    }

    T operator|(T other)
    {
        return T(type_ | other.type_);
    }

private:
    Type type_ {T::NONE};
};

using ProfilingType = ProfileInfo<ProfilingTypeBits>;
using ProfilingTypeOf = ProfileInfo<ProfilingTypeOfBits>;
using ProfilingIndexedAccess = ProfileInfo<ProfilingIndexedAccessBits>;

inline ProfilingTypeBits::Type GetTypeFromValue(JSTaggedValue value)
{
    if (value.IsInt()) {
        return ProfilingTypeBits::INTEGER;
    }
    // IsDouble() is encoded as `!IsInt() && !IsObject()`, IsInt() we already tested, so check only it is not an object
    if (!value.IsObject()) {
        return ProfilingTypeBits::DOUBLE;
    }
    if (value.IsBoolean()) {
        return ProfilingTypeBits::BOOLEAN;
    }
    if (value.IsString()) {
        return ProfilingTypeBits::STRING;
    }
    if (value.IsUndefined()) {
        return ProfilingTypeBits::SPECIAL_DOUBLE;
    }
    if (value.IsNull()) {
        return ProfilingTypeBits::SPECIAL_INT;
    }
    return ProfilingTypeBits::OBJECT;
}

inline ProfilingTypeBits::Type GetTypeFromValueForMod(JSTaggedValue value)
{
    if (value.IsInt()) {
        if (value.GetInt() < 0) {
            return ProfilingTypeBits::DOUBLE_INTEGER;
        }
        return ProfilingTypeBits::INTEGER;
    }
    // IsDouble() is encoded as `!IsInt() && !IsObject()`, IsInt() we already tested, so check only it is not an object
    if (!value.IsObject()) {
        return ProfilingTypeBits::DOUBLE;
    }
    if (value.IsBoolean()) {
        return ProfilingTypeBits::BOOLEAN;
    }
    if (value.IsString()) {
        return ProfilingTypeBits::STRING;
    }
    if (value.IsUndefined()) {
        return ProfilingTypeBits::SPECIAL_DOUBLE;
    }
    if (value.IsNull()) {
        return ProfilingTypeBits::SPECIAL_INT;
    }
    return ProfilingTypeBits::OBJECT;
}
inline ProfilingTypeOfBits::Type GetTypeOfFromValue(JSTaggedValue value)
{
    if (value.IsInt()) {
        return ProfilingTypeOfBits::NUMBER;
    }
    // IsDouble() is encoded as `!IsInt() && !IsObject()`, IsInt() we already tested, so check only it is not an object
    if (!value.IsObject()) {
        return ProfilingTypeOfBits::NUMBER;
    }
    if (value.IsBoolean()) {
        return ProfilingTypeOfBits::BOOLEAN;
    }
    if (value.IsString()) {
        return ProfilingTypeOfBits::STRING;
    }
    if (value.IsBigInt()) {
        return ProfilingTypeOfBits::BIGINT;
    }
    if (value.IsCallable()) {
        return ProfilingTypeOfBits::FUNCTION;
    }
    if (value.IsSymbol()) {
        return ProfilingTypeOfBits::SYMBOL;
    }
    if (value.IsUndefined()) {
        return ProfilingTypeOfBits::UNDEFINED;
    }
    return ProfilingTypeOfBits::OBJECT;
}
ProfilingIndexedAccessBits::Type GetObjectTypeFromValue(JSTaggedValue value);

// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define SET_LAST_FIELD(name) \
    using LastField = name;  \
    static_assert(LastField::END_BIT <= (sizeof(typename Base::ValueType) * BITS_PER_BYTE))

// Check that we don't waste redundant extra bytes for a final profile.
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define SET_LAST_FIELD_FINAL(name) \
    SET_LAST_FIELD(name);          \
    static_assert(LastField::END_BIT > ((sizeof(typename Base::ValueType) - 1) * BITS_PER_BYTE))

template <typename T>
class ValueProfileBase {
public:
    using ValueType = T;

    static_assert(std::is_integral_v<ValueType>, "Only integer type supported");

    ValueProfileBase() = default;
    explicit ValueProfileBase(void *profData)
        // Atomic with acquire order reason: profile data may be updated while the compiler thread loads it
        : value_(profData != nullptr
                     ? reinterpret_cast<std::atomic<ValueType> *>(profData)->load(std::memory_order_acquire)
                     : 0)
    {
    }

    ValueType GetValue() const
    {
        return value_;
    }
    template <typename FieldT>
    void SetField(typename FieldT::ValueType value)
    {
        FieldT::Set(value, &value_);
    }
    template <typename FieldT>
    typename FieldT::ValueType GetField() const
    {
        return FieldT::Get(value_);
    }

private:
    ValueType value_ {0};
};

template <typename T, typename P>
class ResultProfile : public ValueProfileBase<T> {
public:
    using Base = ValueProfileBase<T>;
    using Base::Base;

    P GetResultType() const
    {
        return this->template GetField<ResultType>();
    }

protected:
    using ResultType = BitField<P, 0, MinimumBitsToStore(P::LAST)>;
    SET_LAST_FIELD(ResultType);
};

class ObjByIndexOperationProfile : public ValueProfileBase<uint8_t> {
public:
    using Base = ValueProfileBase<uint8_t>;
    using Base::Base;

    ProfilingIndexedAccess GetOperandType() const
    {
        return ProfilingIndexedAccess(GetField<OperandType>());
    }

    ProfilingIndexedAccess GetOperandType([[maybe_unused]] size_t index) const
    {
        CHECK_LT(index, 1U);
        return ProfilingIndexedAccess(GetOperandType());
    }

    // NOLINTNEXTLINE(readability-non-const-parameter)
    static void Update(void *data, ProfilingIndexedAccessBits::Type accessType)
    {
        // Atomic with release order reason: profile data may be updated while the compiler thread loads it
        reinterpret_cast<std::atomic<Base::ValueType> *>(data)->store(
            *(reinterpret_cast<Base::ValueType *>(data)) | OperandType::Encode(accessType), std::memory_order_release);
    }

protected:
    using OperandType =
        BitField<ProfilingIndexedAccessBits::Type, 0, MinimumBitsToStore(ProfilingIndexedAccessBits::LAST)>;
    SET_LAST_FIELD_FINAL(OperandType);
};

class TypeOfOperationProfile : public ResultProfile<uint16_t, ProfilingTypeOfBits> {
public:
    using Base = ResultProfile<uint16_t, ProfilingTypeOfBits>;
    using Base::Base;

    ProfilingTypeOf GetOperandType() const
    {
        return ProfilingTypeOf(GetField<OperandType>());
    }

    ProfilingTypeOf GetOperandType([[maybe_unused]] size_t index) const
    {
        CHECK_LT(index, 1U);
        return ProfilingTypeOf(GetOperandType());
    }

    static void Update(Base::ValueType *data, JSTaggedValue value)
    {
        *data |= OperandType::Encode(GetTypeOfFromValue(value));
    }

protected:
    using OperandType =
        Base::LastField::NextField<ProfilingTypeOfBits::Type, MinimumBitsToStore(ProfilingTypeOfBits::LAST)>;
    SET_LAST_FIELD_FINAL(OperandType);
};

class UnaryOperationProfile : public ResultProfile<uint16_t, ProfilingTypeBits> {
public:
    using Base = ResultProfile<uint16_t, ProfilingTypeBits>;
    using Base::Base;

    ProfilingType GetOperandType() const
    {
        return ProfilingType(GetField<OperandType>());
    }

    ProfilingType GetOperandType([[maybe_unused]] size_t index) const
    {
        CHECK_LT(index, 1U);
        return ProfilingType(GetOperandType());
    }

    // NOLINTNEXTLINE(readability-non-const-parameter)
    static void Update(Base::ValueType *data, JSTaggedValue value)
    {
        // Atomic with release order reason: profile data may be updated while the compiler thread loads it
        reinterpret_cast<std::atomic<ValueType> *>(reinterpret_cast<uintptr_t>(data))
            ->store(*data | OperandType::Encode(GetTypeFromValue(value)), std::memory_order_release);
    }

protected:
    using OperandType =
        Base::LastField::NextField<ProfilingTypeBits::Type, MinimumBitsToStore(ProfilingTypeBits::LAST)>;
    SET_LAST_FIELD_FINAL(OperandType);
};

class BinaryOperationProfile : public ValueProfileBase<uint16_t> {
public:
    using Base = ValueProfileBase<uint16_t>;
    using Base::Base;

    ProfilingType GetOperandType(size_t index) const
    {
        CHECK_LT(index, 2U);
        return index == 0 ? GetLeftOperandType() : GetRightOperandType();
    }

    ProfilingType GetLeftOperandType() const
    {
        return ProfilingType(GetField<LeftOperandType>());
    }

    ProfilingType GetRightOperandType() const
    {
        return ProfilingType(GetField<RightOperandType>());
    }

    // NOLINTNEXTLINE(readability-non-const-parameter)
    static void Update(Base::ValueType *data, JSTaggedValue lhs, JSTaggedValue rhs)
    {
        auto ltype = GetTypeFromValue(lhs);
        auto rtype = GetTypeFromValue(rhs);
        // Atomic with release order reason: profile data may be updated while the compiler thread loads it
        reinterpret_cast<std::atomic<ValueType> *>(reinterpret_cast<uintptr_t>(data))
            ->store(*data | LeftOperandType::Encode(ltype) | RightOperandType::Encode(rtype),
                    std::memory_order_release);
    }

    // NOLINTNEXTLINE(readability-non-const-parameter)
    static void UpdateMod(Base::ValueType *data, JSTaggedValue lhs, JSTaggedValue rhs)
    {
        auto ltype = GetTypeFromValueForMod(lhs);
        auto rtype = GetTypeFromValueForMod(rhs);
        // Atomic with release order reason: profile data may be updated while the compiler thread loads it
        reinterpret_cast<std::atomic<ValueType> *>(reinterpret_cast<uintptr_t>(data))
            ->store(*data | LeftOperandType::Encode(ltype) | RightOperandType::Encode(rtype),
                    std::memory_order_release);
    }

protected:
    using LeftOperandType = BitField<ProfilingTypeBits::Type, 0, MinimumBitsToStore(ProfilingTypeBits::LAST)>;
    using RightOperandType =
        LeftOperandType::NextField<ProfilingTypeBits::Type, MinimumBitsToStore(ProfilingTypeBits::LAST)>;
    SET_LAST_FIELD_FINAL(RightOperandType);
};

class ECMAObject;

class CallProfile {
    using Type = uint16_t;
    static constexpr uintptr_t MEGAMORPHIC = std::numeric_limits<Type>::max();

public:
    static constexpr Type MAX_FUNC_NUMBER = ark::profiling::MAX_FUNC_NUMBER;
    static constexpr uintptr_t UNKNOWN = 0;
    static CallProfile *FromBuffer(uint8_t *data)
    {
        return reinterpret_cast<CallProfile *>(data);
    }

    ark::profiling::CallKind GetCallKind() const
    {
        // Atomic with acquire order reason: profile data may be updated while the compiler thread loads it
        auto calleeIdx = reinterpret_cast<const std::atomic<Type> *>(&calleesIdx_[0])->load(std::memory_order_acquire);
        if (calleeIdx == MEGAMORPHIC) {
            return ark::profiling::CallKind::MEGAMORPHIC;
        }
        if (calleeIdx == UNKNOWN) {
            return ark::profiling::CallKind::UNKNOWN;
        }
        // Atomic with acquire order reason: profile data may be updated while the compiler thread loads it
        if (reinterpret_cast<const std::atomic<Type> *>(&calleesIdx_[1])->load(std::memory_order_acquire) == UNKNOWN) {
            return ark::profiling::CallKind::MONOMORPHIC;
        }
        return ark::profiling::CallKind::POLYMORPHIC;
    }

    std::array<uintptr_t, MAX_FUNC_NUMBER> GetCalleesPtr(EcmaCallProfilingTable *table) const
    {
        std::array<uintptr_t, MAX_FUNC_NUMBER> objPtrs {};
        for (size_t i = 0; i < MAX_FUNC_NUMBER; ++i) {
            if (calleesIdx_[i] == UNKNOWN) {
                return objPtrs;
            }
            objPtrs[i] = table->GetObjectPtr(calleesIdx_[i]);
        }
        return objPtrs;
    }

    void Clear(EcmaCallProfilingTable *table)
    {
        if (calleesIdx_[0] == MEGAMORPHIC) {
            return;
        }
        for (size_t i = 0; i < MAX_FUNC_NUMBER; ++i) {
            if (calleesIdx_[i] == UNKNOWN) {
                break;
            }
            table->ClearObject(calleesIdx_[i]);
            calleesIdx_[i] = UNKNOWN;
        }
    }

    void Update(ECMAObject *jsFunc, EcmaCallProfilingTable *table);

private:
    std::array<Type, MAX_FUNC_NUMBER> calleesIdx_ {};
    // if you want to increase callees_idx_ size,
    // you need to update size in profiles::Call in ecmascript/isa/isa.yaml
    static_assert(sizeof(calleesIdx_) == 8);
};

/// The following types are used in profile data serialization.
using EcmaProfileElement = std::vector<uint8_t>;
using EcmaProfileContainer = std::unordered_map<std::string, EcmaProfileElement>;
}  // namespace ark::ecmascript

#endif  // PANDA_ECMA_PROFILING_H
